Skip to main content

Real-time Video Comparison:
Touch-Friendly Before/After Slider in Vanilla JS

Draft
Table of Contents

This post contains code for a simple responsive comparison slider that users can drag to reveal the difference between two videos in real-time. The slider is designed to work on both desktop and mobile devices. It includes basic functionality to help keep the videos synchronized.

This is a fork of this picture comparison slider. The following code is distributed under the MIT License.

Live Demo #

Video 2
Video 1

Code #

HTML #

<div class="slider-wrapper">
  <div class="Slider">
    <div id="one" class="bal-container">
      <div class="bal-after">
        <video autoplay loop muted playsinline>
          <source src="videos/FBL.mp4" type="video/mp4" />
        </video>
        <div class="bal-afterPosition afterLabel">Video 2</div>
      </div>
      <div class="bal-before">
        <div class="bal-before-inset">
          <video autoplay loop muted playsinline>
            <source src="videos/no_FBL.mp4" type="video/mp4" />
          </video>
          <div class="bal-beforePosition beforeLabel">Video 1</div>
        </div>
      </div>
      <div class="bal-handle">
        <span class="handle-left-arrow"></span>
        <span class="handle-right-arrow"></span>
      </div>
    </div>
  </div>
</div>

CSS #

<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  .slider-wrapper {
    margin: 0 auto;
    padding: 0px;
    margin-bottom: 25px;
    border-radius: 10px;
    overflow: hidden;
    width: 100%;
    max-width: 100%;
  }

  .Slider {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    justify-content: space-around;
    align-items: stretch;
    align-content: stretch;
    height: 0;
    padding-top: 56.25%;
    position: relative;
  }

  .bal-container {
    margin: 0px 0;
  }

  .bal-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    cursor: grab;
    overflow: hidden;
    font-family: inherit;
    font-weight: inherit;
  }

  .bal-after,
  .bal-before {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 15;
    overflow: hidden;
  }

  .bal-before-inset {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
  }

  .bal-after video,
  .bal-before video {
    object-fit: cover;
    position: absolute;
    width: 100%;
    height: 100%;
    object-position: 50% 50%;
    margin-top: 0;
    margin-bottom: 0;
    left: 0;
    -webkit-user-select: none;
    user-select: none;
    pointer-events: none;
  }

  .bal-beforePosition,
  .bal-afterPosition {
    background: #1e293b;
    color: #e5e9f0;
    pointer-events: none;
    border-radius: 0.2rem;
    padding: 2px 10px;
  }

  .beforeLabel {
    position: absolute;
    top: 0;
    left: 0;
    margin: 1rem;
    font-size: 1.4em;
    user-select: none;
  }

  .afterLabel {
    position: absolute;
    top: 0;
    right: 0;
    margin: 1rem;
    font-size: 1.4em;
    user-select: none;
  }

  .bal-handle {
    height: 41px;
    width: 41px;
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -20px;
    margin-top: -21px;
    border: 3px solid #e5e9f0;
    border-radius: 1000px;
    z-index: 20;
    pointer-events: none;
    box-shadow: 0 0 10px rgb(12, 12, 12);
  }

  .handle-left-arrow,
  .handle-right-arrow {
    width: 0;
    height: 0;
    border: 6px inset transparent;
    position: absolute;
    top: 50%;
    margin-top: -6px;
    scale: 1.3;
  }

  .handle-left-arrow {
    border-right: 6px solid #e5e9f0;
    left: 50%;
    margin-left: -17px;
  }

  .handle-right-arrow {
    border-left: 6px solid #e5e9f0;
    right: 50%;
    margin-right: -17px;
  }

  .bal-handle::before {
    bottom: 50%;
    margin-bottom: 20px;
    box-shadow: 0 0 10px #1e293b;
  }

  .bal-handle::after {
    top: 50%;
    margin-top: 20.5px;
    box-shadow: 0 0 5px #1e293b;
  }

  .bal-handle::before,
  .bal-handle::after {
    content: " ";
    display: block;
    width: 3px;
    background: #e5e9f0;
    height: 480px;
    position: absolute;
    left: 50%;
    margin-left: -1.5px;
  }
</style>

JS #

<script>
  class BeforeAfter {
    constructor(enteryObject) {
      const beforeAfterContainer = document.querySelector(enteryObject.id);
      const before = beforeAfterContainer.querySelector(".bal-before");
      const beforeVideo = before.querySelector("video");
      const afterVideo = beforeAfterContainer.querySelector(".bal-after video");
      const beforeText = beforeAfterContainer.querySelector(
        ".bal-beforePosition"
      );
      const afterText = beforeAfterContainer.querySelector(
        ".bal-afterPosition"
      );
      const handle = beforeAfterContainer.querySelector(".bal-handle");
      let widthChange = 0;

      // Synchronize videos
      const syncVideos = () => {
        beforeVideo.currentTime = 0;
        afterVideo.currentTime = 0;

        const playVideos = () => {
          beforeVideo.play();
          afterVideo.play();
        };

        if (beforeVideo.readyState >= 2 && afterVideo.readyState >= 2) {
          playVideos();
        } else {
          beforeVideo.addEventListener("loadedmetadata", playVideos, { once: true });
          afterVideo.addEventListener("loadedmetadata", playVideos, { once: true });
        }
      };

      syncVideos();

      beforeAfterContainer
        .querySelector(".bal-before-inset")
        .setAttribute(
          "style",
          "width: " + beforeAfterContainer.offsetWidth + "px;"
        );
      window.onresize = function () {
        beforeAfterContainer
          .querySelector(".bal-before-inset")
          .setAttribute(
            "style",
            "width: " + beforeAfterContainer.offsetWidth + "px;"
          );
      };
      before.setAttribute("style", "width: 50%;");
      handle.setAttribute("style", "left: 50%;");

      // Touch screen event listener
      beforeAfterContainer.addEventListener("touchstart", (e) => {
        beforeAfterContainer.addEventListener("touchmove", (e2) => {
          let containerWidth = beforeAfterContainer.offsetWidth;
          let currentPoint = e2.changedTouches[0].clientX;

          let startOfDiv = beforeAfterContainer.offsetLeft;

          let modifiedCurrentPoint = currentPoint - startOfDiv;

          if (
            modifiedCurrentPoint > 10 &&
            modifiedCurrentPoint < beforeAfterContainer.offsetWidth - 10
          ) {
            let newWidth = (modifiedCurrentPoint * 100) / containerWidth;

            before.setAttribute("style", "width:" + newWidth + "%;");
            afterText.setAttribute("style", "z-index: 1;");
            handle.setAttribute("style", "left:" + newWidth + "%;");
          }
        });
      });

      // Mouse move event listener
      beforeAfterContainer.addEventListener("mousemove", (e) => {
        let containerWidth = beforeAfterContainer.offsetWidth;
        widthChange = e.offsetX;
        let newWidth = (widthChange * 100) / containerWidth;

        if (
          e.offsetX > 10 &&
          e.offsetX < beforeAfterContainer.offsetWidth - 10
        ) {
          before.setAttribute("style", "width:" + newWidth + "%;");
          afterText.setAttribute("style", "z-index:" + "1;");
          handle.setAttribute("style", "left:" + newWidth + "%;");
        }
      });
    }
  }

  // Initialize BeforeAfter for the container
  new BeforeAfter({
    id: "#one",
  });
</script>